package ru.sgu.acm.judge.properties;

import org.apache.log4j.Logger;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.nfunk.jep.JEP;
import ru.sgu.acm.judge.misc.Browser;
import static ru.sgu.acm.judge.misc.RetryingStrategy.startRetryingSection;
import static ru.sgu.acm.judge.misc.RetryingStrategy.isTryOneMoreTime;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import static java.lang.String.format;
import java.util.Properties;

/**
 * This class parse form config file and prepare parameters.
 * For more information about configuration file see
 * wiki of pseudocontester project on code google. 
 *
 * @author Vitaly Goldshteyn (VitalyGoldstein@gmail.com)
 */
public class ParametersConfig {

    Logger logger = Logger.getLogger(ParametersConfig.class);

    Properties config, parsed;
    private final String IN_WORK = "~IN~WORK~";
    private String name;

    public ParametersConfig(Properties config) {
        this.config = config;
        name = config.getProperty("name");
        parsed = new Properties();
    }

    /**
     * Set property, which not set in config ini.
     * @param key key
     * @param value value
     */
    public void setProperty(String key, String value) {
        config.setProperty(key, value);
    }

    /**
     * @return parsed and dowloading parameters.
     * @throws Exception if unable to parse or download parameters.
     */
    public Properties getParameters() throws Exception {
        startRetryingSection("processConstantParameters");
        processConstantParameters();
        String parametrPrefix = name + ".parameter.";
        Properties result = new Properties();
        for (String key : config.stringPropertyNames()) {
            if (key.startsWith(parametrPrefix)) {
                result.setProperty(key.substring(parametrPrefix.length()),
                        parseForKey(key));
            }
        }
        return result;
    }

    /**
     * Download form entity and set hidden and submit parameters.
     * @throws Exception if unable to download or parse form content.
     */
    private void processConstantParameters() throws Exception {
        String contentUrl = getFormContentUrl();
        if (contentUrl != null) {
            try {
                logger.info("Getting form content...");
                Node document = Browser.getHtmlDocumentFromCurrentSession(contentUrl);
                String action = getAction();
                XPath xPath = XPathFactory.newInstance().newXPath();
                XPathExpression formExpression = xPath.compile(format("//FORM[@action='%s']", action));
                Node form = (Node)formExpression.evaluate(document, XPathConstants.NODE);
                XPathExpression inputExpression = xPath.compile("descendant::INPUT");
                NodeList inputs = (NodeList)inputExpression.evaluate(form, XPathConstants.NODESET);
                for(int i = 0; i < inputs.getLength(); ++i) {
                    Node input = inputs.item(i);
                    NamedNodeMap attributes = input.getAttributes();
                    String type = getAttributeValue(attributes, "type", "");
                    if (type.equalsIgnoreCase("submit") || type.equalsIgnoreCase("hidden")) {
                        String nameAtt = getAttributeValue(attributes, "name", type);
                        String valueAtt = getAttributeValue(attributes, "value", "");
                        logger.info(name + ".parameter." + nameAtt + "=" + valueAtt);
                        config.setProperty(name + ".parameter." + nameAtt, valueAtt);
                    }
                }

            } catch (Exception e) {
                logger.warn("Unable to get form content, trying again... ", e);
                if (isTryOneMoreTime("processConstantParameters")) {
                    processConstantParameters();
                } else {
                    throw(e);
                }
            }
        }
    }

    private String getAttributeValue(NamedNodeMap attributes, String key, String def) {
        Node node = attributes.getNamedItem(key);
        if (node == null) {
            return def;
        }
        return node.getNodeValue();
    }

    private String getAction() {
        return parseForKey(name + ".action");
    }

    private String getFormContentUrl() {
        String key = name + ".contenturl";
        if (config.containsKey(key)) {
            return parseForKey(key);
        } else {
            return null;
        }
    }

    /**
     * @return url for submitting form or download page.
     */
    public String getUrl() {
        return parseForKey(name + ".url");
    }

    /**
     * parse value for key
     * @param key parameter key
     * @return parsed value
     */
    private String parseForKey(String key) {
        if (!key.contains(".")) {
            return config.getProperty(key);
        }
        String value = parsed.getProperty(key);
        if (value != null) {
            if (value.equals(IN_WORK)) {
                throw new IllegalStateException("cycle link in confg file");
            }
            return value;
        } else {
            parsed.setProperty(key, IN_WORK);
            value = config.getProperty(key);
        }
        if (value == null) {
            throw new IllegalStateException("key " + key + " is not initialized");
        }
        value = parseForValue(value);
        parsed.setProperty(key, value);
        return value;
    }

    /**
     * parse for value
     * @param value value
     * @return parsed value
     */
    private String parseForValue(String value) {
        if (value.startsWith("'")) {
            return value.substring(1, value.length() - 1);
        }
        int posSharp = value.indexOf('#');
        if (posSharp != -1) {
            String key = value.substring(posSharp + 2);
            int posCloseBracket = key.indexOf('}');
            key = key.substring(0, posCloseBracket);
            int length = value.length();
            value = value.substring(0, posSharp) + parseForKey(key) +
                    value.substring(posSharp + posCloseBracket + 3);
            if (posSharp != 0 || posSharp + posCloseBracket + 3 < length) {
                value = parseForValue(value);
            }
        } else {
            int posBracket = value.indexOf('[');
            if (posBracket != -1) {
                String mapName = value.substring(0, posBracket);
                String mapKey = value.substring(posBracket + 1, value.length() - 1);
                if (mapName.equals("Expression")) {
                    value = calculateExpression(parseForValue(mapKey));
                } else {
                    value = parseForKey(name + ".map." + mapName + "." + parseForValue(mapKey));
                }
            }
        }
        return value;
    }

    /**
     * Calculate any expression with statndard functions in JEP.
     * @param expression expression
     * @return result integer as String
     * @throws NumberFormatException if unable to parse expression
     */
    private String calculateExpression(String expression) throws NumberFormatException {
        JEP jep = new JEP();
        jep.addStandardFunctions();
        jep.parseExpression(expression);
        if (jep.hasError()) {
            throw new NumberFormatException(jep.getErrorInfo());
        }
        String res = "" + Double.valueOf(jep.getValue()).longValue();
        if (jep.hasError()) {
            throw new NumberFormatException(jep.getErrorInfo());
        }
        return res;
    }
}
